/*
 *  Openmysee
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
#include "stdafx.h"
#include "BalloonHelp.h"

#ifndef DFCS_HOT
#define DFCS_HOT 0x1000
#endif

#ifndef WS_EX_LAYERED
#define WS_EX_LAYERED   0x00080000
#define LWA_COLORKEY    0x00000001
#define LWA_ALPHA       0x00000002
#endif

#ifndef CS_DROPSHADOW
#define CS_DROPSHADOW   0x00020000
#endif

#ifndef SPI_GETDROPSHADOW
#define SPI_GETDROPSHADOW  0x1024
#endif

#ifndef SPI_GETTOOLTIPANIMATION
#define SPI_GETTOOLTIPANIMATION 0x1016
#endif

#ifndef SPI_GETTOOLTIPFADE
#define SPI_GETTOOLTIPFADE 0x1018
#endif

/////////////////////////////////////////////////////////////////////////////
// CBalloonHelp

// these could go in resources, if they make you nervous
// if someone knows a better way to deal with timers than to use
// constant IDs, pls let me know?
#define ID_TIMER_SHOW      3333
#define ID_TIMER_HIDE      3334
#define ID_TIMER_CLOSE     3335
#define ID_TIMER_HOTTRACK  3336

// option constants (bits)
const unsigned int   CBalloonHelp::unCLOSE_ON_LBUTTON_UP = 0x001;
const unsigned int   CBalloonHelp::unCLOSE_ON_RBUTTON_UP = 0x002;
const unsigned int   CBalloonHelp::unCLOSE_ON_MOUSE_MOVE = 0x004;
const unsigned int   CBalloonHelp::unCLOSE_ON_KEYPRESS   = 0x008;
const unsigned int   CBalloonHelp::unDELETE_THIS_ON_CLOSE= 0x010;
const unsigned int   CBalloonHelp::unSHOW_CLOSE_BUTTON   = 0x020;
const unsigned int   CBalloonHelp::unSHOW_INNER_SHADOW   = 0x040;
const unsigned int   CBalloonHelp::unSHOW_TOPMOST        = 0x080;
const unsigned int   CBalloonHelp::unDISABLE_FADEIN      = 0x100;
const unsigned int   CBalloonHelp::unDISABLE_FADEOUT     = 0x200;
const unsigned int	 CBalloonHelp::unHIDE_AFTER_CREATED  = 0x400;
const unsigned int   CBalloonHelp::unDISABLE_FADE        = CBalloonHelp::unDISABLE_FADEIN|CBalloonHelp::unDISABLE_FADEOUT;

// layout constants (should prolly be configurable)
const int            CBalloonHelp::nTIP_TAIL             = 20;
const int            CBalloonHelp::nTIP_MARGIN           = 8;
// synchronization for keyboard hook
CComCriticalSection     CBalloonHelp::s_KeyboardHookSection;
// windows using keyboard hook
CAtlArray<HWND>      CBalloonHelp::s_apKeyboardCloseWnds;
// handle to the keyboard hook, if set
HHOOK                CBalloonHelp::s_hKeyboardHook       = NULL;

void FillSolidRect(HDC hdc, const RECT *pRect, COLORREF clr)
{
	SetBkColor(hdc, clr);
	ExtTextOut(hdc, 0, 0, ETO_OPAQUE, pRect, NULL, 0, NULL);
}

CBalloonHelp::CBalloonHelp() :
	m_fnSetLayeredWindowAttributes(NULL),
	m_unOptions(0),
	m_unTimeout(0),
	m_strURL(""),
	m_strContent(""),
	m_nAlpha(255),
	m_nMouseMoveTolerance(3),
	m_uCloseState(0),
	m_pTitleFont(NULL),
	m_pContentFont(NULL),
	m_crForeground(::GetSysColor(COLOR_INFOTEXT)),
	m_crBackground(::GetSysColor(COLOR_INFOBK)),
	m_ilIcon(NULL),
	m_rgnComplete(NULL)
{
	s_KeyboardHookSection.Init();
	m_ptAnchor.x = m_ptAnchor.y = 0;
	m_ptMouseOrig.x = m_ptMouseOrig.y = 0;
	// retrieve layered window API if available
	HMODULE hUser32 = GetModuleHandle(_T("USER32.DLL"));
	m_fnSetLayeredWindowAttributes = (FN_SET_LAYERED_WINDOW_ATTRIBUTES)GetProcAddress(hUser32, _T("SetLayeredWindowAttributes"));
}

CBalloonHelp::~CBalloonHelp()
{
	if ( NULL != m_pTitleFont )
		DeleteObject(m_pTitleFont);
	m_pTitleFont = NULL;
	if ( NULL != m_pContentFont )
		DeleteObject(m_pContentFont);
	m_pContentFont = NULL;
	if (m_ilIcon)
		ImageList_Destroy(m_ilIcon);
	if (m_rgnComplete)
		DeleteObject(m_rgnComplete);
}



//
// Show a help balloon on screen
// Parameters:
//    strTitle    |  Title of balloon
//    strContent  |  Content of balloon
//    ptAnchor    |  point tail of balloon will be "anchor"ed to
//    szIcon      |  One of:
//                   IDI_APPLICATION
//                   IDI_INFORMATION IDI_ASTERISK (same)
//                   IDI_ERROR IDI_HAND (same)
//                   IDI_EXCLAMATION IDI_WARNING (same)
//                   IDI_QUESTION
//                   IDI_WINLOGO
//                   NULL (no icon)
//    unOptions   |  One or more of:
//                :     unCLOSE_ON_LBUTTON_UP   |  closes window on WM_LBUTTON_UP
//                :     unCLOSE_ON_RBUTTON_UP   |  closes window on WM_RBUTTON_UP
//                :     unCLOSE_ON_MOUSE_MOVE   |  closes window when user moves mouse past threshhold
//                :     unCLOSE_ON_KEYPRESS     |  closes window on the next keypress message sent to this thread.  (!!! probably not thread safe !!!)
//                :     unSHOW_CLOSE_BUTTON     |  shows close button in upper right
//                :     unSHOW_INNER_SHADOW     |  draw inner shadow in balloon
//                :     unSHOW_TOPMOST          |  place balloon above all other windows
//                :     unDISABLE_FADE          |  disable the fade-in/fade-out effects (overrides system and user settings)
//                :     unDISABLE_FADEIN        |  disable the fade-in effect
//                :     unDISABLE_FADEOUT       |  disable the fade-out effect
//    pParentWnd  |  Parent window.  If NULL will be set to AfxGetMainWnd()
//    strURL      |  If not empty, when the balloon is clicked ShellExecute() will
//                |  be called, with strURL passed in.
//    unTimeout   |  If not 0, balloon will automatically close after unTimeout milliseconds.
//



void CBalloonHelp::LaunchBalloon(const CString& strTitle, const CString& strContent, 
               const CPoint& ptAnchor, 
               LPCTSTR szIcon,                //= IDI_EXCLAMATION
               unsigned int unOptions, // unSHOW_CLOSE_BUTTON
               HWND pParentWnd ,//= NULL
               const CString strURL, //= ""
               unsigned int unTimeout)//= 10000
{
   CBalloonHelp* pbh = new CBalloonHelp;
   if ( NULL != szIcon )
   {
      HICON hIcon = (HICON)::LoadImage(NULL, szIcon, IMAGE_ICON, 16,16, LR_SHARED);
      if (NULL != hIcon)
      {
         // Use a scaled standard icon (looks very good on Win2k, XP, not so good on Win9x)
         CDC dc;
         CDC dcTmp1;
         CDC dcTmp2;
         CBitmap bmpIcon;
         CBitmap bmpIconSmall;
         dc.Attach(::GetDC(NULL));
         dcTmp1.CreateCompatibleDC(&dc);
         dcTmp2.CreateCompatibleDC(&dc);
         bmpIcon.CreateCompatibleBitmap(&dc, 32,32);
         bmpIconSmall.CreateCompatibleBitmap(&dc, 16,16);
         ::ReleaseDC(NULL, dc.Detach());
         
         // i now have two device contexts and two bitmaps.
         // i will select a bitmap in each device context,
         // draw the icon into the larger one,
         // scale it into the smaller one,
         // and set the small one as the balloon icon.
         // This is a rather long process to get a small icon,
         // but ensures maximum compatibility between different
         // versions of Windows, while producing the best possible
         // results on each version.
         CBitmap* pbmpOld1 = dcTmp1.SelectObject(&bmpIcon);
         CBitmap* pbmpOld2 = dcTmp2.SelectObject(&bmpIconSmall);
         dcTmp1.FillSolidRect(0,0,32,32, pbh->m_crBackground);
         ::DrawIconEx(dcTmp1, 0,0,hIcon,32,32,0,NULL,DI_NORMAL);
         dcTmp2.SetStretchBltMode(HALFTONE);
         dcTmp2.StretchBlt(0,0,16,16,&dcTmp1, 0,0,32,32,SRCCOPY);
         dcTmp1.SelectObject(pbmpOld1);
         dcTmp2.SelectObject(pbmpOld2);

         pbh->SetIcon(bmpIconSmall, pbh->m_crBackground);
      }
   }

   pbh->Create(strTitle, strContent, ptAnchor, unOptions|unDELETE_THIS_ON_CLOSE, 
               pParentWnd, strURL, unTimeout, NULL);
}


// Sets the font used for drawing the balloon title.  Deleted by balloon, do not use CFont* after passing to this function.
void CBalloonHelp::SetTitleFont(HFONT pFont)
{
   if ( NULL != m_pTitleFont )
      DeleteObject(m_pTitleFont);
   m_pTitleFont = pFont;
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the font used for drawing the balloon content.  Deleted by balloon, do not use CFont* after passing to this function.
void CBalloonHelp::SetContentFont(HFONT pFont)
{
   if ( NULL != m_pContentFont )
      DeleteObject(m_pContentFont);
   m_pContentFont = pFont;
   // if already visible, resize & move
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon (pass NULL to hide icon)
void CBalloonHelp::SetIcon(HICON hIcon)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);
   ICONINFO iconinfo;
   if ( NULL != hIcon && ::GetIconInfo(hIcon, &iconinfo) )
   {
      SetIcon(iconinfo.hbmColor, iconinfo.hbmMask);
      ::DeleteObject(iconinfo.hbmColor);
      ::DeleteObject(iconinfo.hbmMask);
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon (pass NULL hBitmap to hide icon)
void CBalloonHelp::SetIcon(HBITMAP hBitmap, COLORREF crMask)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);

   if ( NULL != hBitmap )
   {
      BITMAP bm;
      if (::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
      {
         m_ilIcon = ImageList_Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK, 1, 0);
         ImageList_AddMasked(m_ilIcon, hBitmap, crMask);
      }
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the icon displayed in the top left of the balloon
void CBalloonHelp::SetIcon(HBITMAP hBitmap, HBITMAP hMask)
{
   if ( NULL != m_ilIcon )
      ImageList_Destroy(m_ilIcon);

   _ASSERTE(hBitmap);
   _ASSERTE(hMask);

   BITMAP bm;
   if (::GetObject(hBitmap, sizeof(bm),(LPVOID)&bm))
   {
      m_ilIcon = ImageList_Create(bm.bmWidth, bm.bmHeight, ILC_COLOR24|ILC_MASK, 1, 0);
      ImageList_Add(m_ilIcon, hBitmap, hMask);
   }
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Set icon displayed in the top left of the balloon to image # nIconIndex from pImageList
void CBalloonHelp::SetIcon(HIMAGELIST pImageList, int nIconIndex)
{
   // sanity checks
   _ASSERTE(pImageList);
   _ASSERTE(nIconIndex >= 0 && nIconIndex < ImageList_GetImageCount(pImageList) );

   HICON hIcon = NULL;
   if ( NULL != pImageList && nIconIndex >= 0 && nIconIndex < ImageList_GetImageCount(pImageList) )
      hIcon = ImageList_ExtractIcon(NULL, pImageList, nIconIndex);
   SetIcon(hIcon);
   if ( NULL != hIcon )
      ::DestroyIcon(hIcon);
   // if already visible, resize & move (icon size may have changed)
   if ( NULL != m_hWnd )
      PositionWindow();
}

// Sets the URL to be opened when balloon is clicked.  Pass "" to disable.
void CBalloonHelp::SetURL(const CString& strURL)
{
   m_strURL = strURL;
}

// Sets the number of milliseconds the balloon can remain open.  Set to 0 to disable timeout.
void CBalloonHelp::SetTimeout(unsigned int unTimeout)
{
   m_unTimeout = unTimeout;
   // if timer is already set, reset.
   if ( NULL != m_hWnd )
   {
      if ( m_unTimeout > 0 )
         SetTimer(ID_TIMER_CLOSE, m_unTimeout, NULL);
      else
         KillTimer(ID_TIMER_CLOSE);
   }
}

// Sets the point to which the balloon is "anchored"
void CBalloonHelp::SetAnchorPoint(CPoint ptAnchor)
{
   m_ptAnchor = ptAnchor;

   // if already visible, move
   if ( NULL != m_hWnd )
   {
      PositionWindow();
   }
}

// Sets the title of the balloon
void CBalloonHelp::SetTitle(const CString& strTitle)
{
   if(m_strTitle != strTitle)
   {
      m_strTitle = strTitle;
      SetWindowText(m_strTitle);
      // if already visible, resize & move
      if ( NULL != m_hWnd )
         PositionWindow(true);
   }
}

// Sets the content of the balloon (plain text only)
void CBalloonHelp::SetContent(const CString& strContent)
{
   if(m_strContent != strContent)
   {
      m_strContent = strContent;
      // if already visible, resize & move
      if ( NULL != m_hWnd )
         PositionWindow(true);
   }
}

// Sets the forground (text and border) color of the balloon
void CBalloonHelp::SetForegroundColor(COLORREF crForeground)
{
   m_crForeground = crForeground;
   // repaint if visible
   if ( NULL != m_hWnd )
      Invalidate(FALSE);
}

// Sets the background color of the balloon
void CBalloonHelp::SetBackgroundColor(COLORREF crBackground)
{
   m_crBackground = crBackground;
   // repaint if visible
   if ( NULL != m_hWnd )
      Invalidate(FALSE);
}

// Sets the distance the mouse must move before the balloon closes when the unCLOSE_ON_MOUSE_MOVE option is set.
void CBalloonHelp::SetMouseMoveTolerance(int nTolerance)
{
   m_nMouseMoveTolerance = nTolerance;
}

//
// creates a new balloon window
// Parameters:
//    strTitle    |  Title of balloon
//    strContent  |  Content of balloon
//    ptAnchor    |  point tail of balloon will be "anchor"ed to
//    unOptions   |  One or more of:
//                :     unCLOSE_ON_LBUTTON_UP   |  closes window on WM_LBUTTON_UP
//                :     unCLOSE_ON_RBUTTON_UP   |  closes window on WM_RBUTTON_UP
//                :     unCLOSE_ON_MOUSE_MOVE   |  closes window when user moves mouse past threshhold
//                :     unCLOSE_ON_KEYPRESS     |  closes window on the next keypress message sent to this thread.    (!!! probably not thread safe !!!)
//                :     unDELETE_THIS_ON_CLOSE  |  deletes object when window is closed.  Used by LaunchBalloon(), use with care
//                :     unSHOW_CLOSE_BUTTON     |  shows close button in upper right
//                :     unSHOW_INNER_SHADOW     |  draw inner shadow in balloon
//                :     unSHOW_TOPMOST          |  place balloon above all other windows
//                :     unDISABLE_FADE          |  disable the fade-in/fade-out effects (overrides system and user settings)
//                :     unDISABLE_FADEIN        |  disable the fade-in effect
//                :     unDISABLE_FADEOUT       |  disable the fade-out effect
//    pParentWnd  |  Parent window.  If NULL will be set to AfxGetMainWnd()
//    strURL      |  If not empty, when the balloon is clicked ShellExecute() will
//                |  be called, with strURL passed in.
//    unTimeout   |  If not 0, balloon will automatically close after unTimeout milliseconds.
//    hIcon       |  If not NULL, the icon indicated by hIcon will be displayed at top-left of the balloon.
//
// Returns:
//    TRUE if successful, else FALSE
//
BOOL CBalloonHelp::Create(const CString& strTitle, const CString& strContent, 
               const CPoint& ptAnchor, unsigned int unOptions,
               HWND pParentWnd, //=NULL
               const CString strURL, //= ""
               unsigned int unTimeout, //= 0
               HICON hIcon) //= NULL
{
	m_strContent   = strContent;
	m_ptAnchor     = ptAnchor;
	m_unOptions    = unOptions;
	m_strURL       = strURL;
	m_unTimeout    = unTimeout;

	if ( NULL != hIcon )
		SetIcon(hIcon);
	   
	if (NULL == pParentWnd)
		return FALSE;

	// if no fonts set, use defaults
	if ( NULL == m_pContentFont )
	{
		m_pContentFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
		if (!m_pContentFont)
			return FALSE;
	}

	// title font defaults to bold version of content font
	if ( NULL == m_pTitleFont )
	{
		LOGFONT LogFont;
		GetObject(m_pContentFont, sizeof(LogFont), &LogFont);
		LogFont.lfWeight = FW_BOLD;
		if (!(m_pTitleFont = (HFONT)CreateFontIndirect(&LogFont)))
			return FALSE;
	}

	// check system settings: if fade effects are disabled, disable here too
	BOOL bFade = FALSE;
	::SystemParametersInfo(SPI_GETTOOLTIPANIMATION, 0, &bFade, 0);
	if (bFade)
		::SystemParametersInfo(SPI_GETTOOLTIPFADE, 0, &bFade, 0);
	if (!bFade || NULL == m_fnSetLayeredWindowAttributes)
		m_unOptions |= unDISABLE_FADE;

	// create invisible at arbitrary position; then position, set region, and finally show

	// the idea with WS_EX_TOOLWINDOW is, you can't switch to this using alt+tab
	DWORD dwExStyle = WS_EX_TOOLWINDOW;
    if(!((m_unOptions & unDISABLE_FADE) == unDISABLE_FADE))
        dwExStyle |= WS_EX_LAYERED;

	if ( m_unOptions&unSHOW_TOPMOST )      // make topmost, if requested
		dwExStyle |= WS_EX_TOPMOST;
	if (!__super::Create(pParentWnd, CRect(0, 0, 10, 10), strTitle, WS_POPUP, dwExStyle))
		return FALSE;
	PositionWindow();

	// show, possibly fading
	if(!(m_unOptions & unHIDE_AFTER_CREATED))
		ShowBalloon();

	if ( m_unOptions & unCLOSE_ON_MOUSE_MOVE )
	{
		::GetCursorPos(&m_ptMouseOrig);
		ScreenToClient(&m_ptMouseOrig);
		SetTimer(ID_TIMER_HOTTRACK, 100, NULL);
	}

	if ( (m_unOptions & unCLOSE_ON_LBUTTON_UP) || (m_unOptions & unCLOSE_ON_RBUTTON_UP) )
		SetCapture();

	if ( m_unOptions&unCLOSE_ON_KEYPRESS )
		SetKeyboardHook();

	// show, but don't take focus away from parent (or whatever)
//	ShowWindow(SW_SHOWNOACTIVATE);

	return TRUE;
}

// Wrapper for possibly-existing API call
BOOL CBalloonHelp::SetLayeredWindowAttributes(COLORREF crKey, int nAlpha, DWORD dwFlags)
{
   if ( NULL != m_fnSetLayeredWindowAttributes )
      return (*m_fnSetLayeredWindowAttributes)(m_hWnd, crKey, (BYTE)nAlpha, dwFlags);
   return FALSE;
}

// calculates the area of the screen the balloon falls into
// this determins which direction the tail points
CBalloonHelp::BALLOON_QUADRANT CBalloonHelp::GetBalloonQuadrant()
{
   CRect rectDesktop;
   ::GetWindowRect(::GetDesktopWindow(), &rectDesktop);
   
   if ( m_ptAnchor.y < rectDesktop.Height()/2 )
   {
      if ( m_ptAnchor.x < rectDesktop.Width()/2 )
      {
         return BQ_TOPLEFT;
      }
      else
      {
         return BQ_TOPRIGHT;
      }
   }
   else
   {
      if ( m_ptAnchor.x < rectDesktop.Width()/2 )
      {
         return BQ_BOTTOMLEFT;
      }
      else
      {
         return BQ_BOTTOMRIGHT;
      }
   }

   // unreachable
}

// Calculate the dimensions and draw the balloon header
CSize CBalloonHelp::DrawHeader(HDC pDC, bool bDraw)
{
   CSize sizeHdr(0,0);
   CRect rectClient;
   GetClientRect(&rectClient);   // use this for positioning when drawing
                                 // else if content is wider than title, centering wouldn't work

   // calc & draw icon
   if ( NULL != m_ilIcon )
   {
      int x = 0;
      int y = 0;
      ImageList_GetIconSize(m_ilIcon, &x, &y);
      sizeHdr.cx += x;
      sizeHdr.cy = max(sizeHdr.cy, y);
      ImageList_SetBkColor(m_ilIcon, m_crBackground);
      if (bDraw)
         ImageList_Draw(m_ilIcon, 0, pDC, 0, 0, ILD_NORMAL);//ILD_TRANSPARENT);
      rectClient.left += x;
   }
   // make some space between left icon and title
   rectClient.left += 5;

   // calc & draw close button
   if ( m_unOptions & unSHOW_CLOSE_BUTTON )
   {
      // if something is already in the header (icon) leave space
      if ( sizeHdr.cx > 0 )
         sizeHdr.cx += nTIP_MARGIN;
      sizeHdr.cx += 16;
      sizeHdr.cy = max(sizeHdr.cy, 16);
      if (bDraw)
		 DrawFrameControl(pDC, CRect(rectClient.right-16,0,rectClient.right,16), DFC_CAPTION, DFCS_CAPTIONCLOSE|DFCS_FLAT);
      rectClient.right -= 16;
   }

   // calc title size
   CString strTitle;
   GetWindowText(strTitle);
   if ( !strTitle.IsEmpty() )
   {
      HFONT pOldFont = (HFONT)SelectObject(pDC, m_pTitleFont);

      // if something is already in the header (icon or close button) leave space
      if ( sizeHdr.cx > 0 ) 
         sizeHdr.cx += nTIP_MARGIN;
      CRect rectTitle(0,0,0,0);
	  DrawText(pDC, strTitle, strTitle.GetLength(), &rectTitle, DT_CALCRECT | DT_NOPREFIX | DT_EXPANDTABS | DT_SINGLELINE);
      sizeHdr.cx += rectTitle.Width();
      sizeHdr.cy = max(sizeHdr.cy, rectTitle.Height());

	  //slightly adjust the y position of title to center it vertically with the left icon
	  rectClient.top = (sizeHdr.cy - rectTitle.Height()) /2;
      // draw title
      if ( bDraw )
      {
         SetBkMode(pDC, TRANSPARENT);
         SetTextColor(pDC, m_crForeground);
         DrawText(pDC, strTitle, strTitle.GetLength(), &rectClient,  DT_NOPREFIX  | DT_EXPANDTABS | DT_SINGLELINE);
      }

      // cleanup
      SelectObject(pDC, pOldFont);
   }

   return sizeHdr;
}

// Calculate the dimensions and draw the balloon contents
CSize CBalloonHelp::DrawContent(HDC pDC, int nTop, bool bDraw)
{
   CRect rectContent;
   ::GetClientRect(::GetDesktopWindow(), &rectContent);

   rectContent.top = nTop;

   // limit to half screen width
   rectContent.right -= rectContent.Width()/2;

   // calc size
   HFONT pOldFont = (HFONT)SelectObject(pDC, m_pContentFont);
   if ( !m_strContent.IsEmpty() )
      DrawText(pDC, m_strContent, m_strContent.GetLength(), &rectContent, DT_CALCRECT | DT_LEFT | DT_NOPREFIX | DT_EXPANDTABS | DT_WORDBREAK);
   else
      rectContent.SetRectEmpty();   // don't want to leave half the screen for empty strings ;)
   
   // draw
   if (bDraw)
   {
      SetBkMode(pDC, TRANSPARENT);
      SetTextColor(pDC, m_crForeground);
      DrawText(pDC, m_strContent, m_strContent.GetLength(), &rectContent, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK | DT_EXPANDTABS);
   }

   // cleanup
   SelectObject(pDC, pOldFont);

   return rectContent.Size();
}

// calculates the client size necessary based on title and content
CSize CBalloonHelp::CalcClientSize()
{
   _ASSERTE(m_hWnd);
   HDC hdc = GetDC();
   CSize sizeHeader = CalcHeaderSize(hdc);
   CSize sizeContent = CalcContentSize(hdc);
   ReleaseDC(hdc);

   return CSize(max(sizeHeader.cx,sizeContent.cx), sizeHeader.cy + nTIP_MARGIN + sizeContent.cy);
}

// calculates the size for the entire window based on content size
CSize CBalloonHelp::CalcWindowSize()
{
   CSize size = CalcClientSize();
   size.cx += nTIP_MARGIN*2;
   size.cy += nTIP_TAIL+nTIP_MARGIN*2;
   //size.cx = max(size.cx, nTIP_MARGIN*2+nTIP_TAIL*4);
   return size;
}


// this routine calculates the size and position of the window relative
// to it's anchor point, and moves the window accordingly.  The region is also
// created and set here.
void CBalloonHelp::PositionWindow(bool forceRedraw/*=false*/)
{
	CSize sizeWnd = CalcWindowSize();

	CPoint ptTail[3];
	CPoint ptTopLeft(0, 0);
	CPoint ptBottomRight(sizeWnd.cx, sizeWnd.cy);

	switch (GetBalloonQuadrant())
	{
	case BQ_TOPLEFT:
		ptTopLeft.y = nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4 + nTIP_TAIL;
		ptTail[0].y = nTIP_TAIL+1;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQ_TOPRIGHT:
		ptTopLeft.y = nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4*3;
		ptTail[0].y = nTIP_TAIL+1;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4*3 + nTIP_TAIL;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = 1;
		break;
	case BQ_BOTTOMLEFT:
		ptBottomRight.y = sizeWnd.cy-nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4 + nTIP_TAIL;
		ptTail[0].y = sizeWnd.cy-nTIP_TAIL-2;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = sizeWnd.cy-2;
		break;
	case BQ_BOTTOMRIGHT:
		ptBottomRight.y = sizeWnd.cy-nTIP_TAIL;
		ptTail[0].x = (sizeWnd.cx-nTIP_TAIL)/4*3;
		ptTail[0].y = sizeWnd.cy-nTIP_TAIL-2;
		ptTail[2].x = (sizeWnd.cx-nTIP_TAIL)/4*3 + nTIP_TAIL;
		ptTail[2].y = ptTail[0].y;
		ptTail[1].x = ptTail[2].x;
		ptTail[1].y = sizeWnd.cy-2;
		break;
	}

	// adjust for very narrow balloons
	if ( ptTail[0].x < nTIP_MARGIN )
		ptTail[0].x = nTIP_MARGIN;
	if ( ptTail[0].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[0].x = sizeWnd.cx - nTIP_MARGIN;
	if ( ptTail[1].x < nTIP_MARGIN )
		ptTail[1].x = nTIP_MARGIN;
	if ( ptTail[1].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[1].x = sizeWnd.cx - nTIP_MARGIN;
	if ( ptTail[2].x < nTIP_MARGIN )
		ptTail[2].x = nTIP_MARGIN;
	if ( ptTail[2].x > sizeWnd.cx - nTIP_MARGIN )
		ptTail[2].x = sizeWnd.cx - nTIP_MARGIN;

	// get window position
	CPoint ptOffs(m_ptAnchor.x - ptTail[1].x, m_ptAnchor.y - ptTail[1].y);

	// adjust position so all is visible
	CRect rectScreen;
	::GetWindowRect(::GetDesktopWindow(), &rectScreen);
	int nAdjustX = 0;
	int nAdjustY = 0;
	if ( ptOffs.x < 0 )
		nAdjustX = -ptOffs.x;
	else if ( ptOffs.x + sizeWnd.cx >= rectScreen.right )
		nAdjustX = rectScreen.right - (ptOffs.x + sizeWnd.cx);
	if ( ptOffs.y < 0 )
		nAdjustY = -ptOffs.y;
	else if ( ptOffs.y + sizeWnd.cy >= rectScreen.bottom )
		nAdjustY = rectScreen.bottom - (ptOffs.y + sizeWnd.cy);

	// reposition tail
	// uncomment two commented lines below to move entire tail 
	// instead of just anchor point

	//ptTail[0].x -= nAdjustX;
	ptTail[1].x -= nAdjustX;
	//ptTail[2].x -= nAdjustX;
	ptOffs.x    += nAdjustX;
	ptOffs.y    += nAdjustY;

	// place window
	MoveWindow(ptOffs.x, ptOffs.y, sizeWnd.cx, sizeWnd.cy, TRUE);

	// apply region
	BOOL bRegionChanged = TRUE;
	HRGN region;		//the tail of balloon
	HRGN regionRound;	//the body of balloon
	HRGN regionComplete;//the total part
	region = CreatePolygonRgn(&ptTail[0], 3, ALTERNATE);	
	regionRound = CreateRoundRectRgn(ptTopLeft.x,ptTopLeft.y,ptBottomRight.x,ptBottomRight.y,nTIP_MARGIN*2,nTIP_MARGIN*2);
	regionComplete = CreateRectRgn(0,0,1,1);
	CombineRgn(regionComplete, region, regionRound, RGN_OR);

	if ( NULL == m_rgnComplete )
		m_rgnComplete = CreateRectRgn(0, 0, 1, 1);
	else if ( EqualRgn(m_rgnComplete, regionComplete) )
		bRegionChanged = FALSE;
	::CombineRgn(m_rgnComplete, regionComplete, NULL, RGN_COPY);
	   
	SetWindowRgn(regionComplete, TRUE);
	DeleteObject(region);
	DeleteObject(regionRound);

	// There is a bug with layered windows and NC changes in Win2k
	// As a workaround, redraw the entire window if the NC area changed.
	// Changing the anchor point is the ONLY thing that will change the
	// position of the client area relative to the window during normal
	// operation.
	if ( bRegionChanged || forceRedraw)
		RedrawWindow(NULL, NULL, RDW_UPDATENOW| RDW_ERASE | RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN);
}



// Displays the balloon on the screen, performing fade-in if enabled.
void CBalloonHelp::ShowBalloon(void)
{
   if(!IsWindowVisible())
  {
	  ShowWindow(SW_SHOWNOACTIVATE);
	  m_nAlpha = 5;
	  SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
      UINT_PTR i = SetTimer(ID_TIMER_SHOW, 25, NULL);
   }
}

// Removes the balloon from the screen, performing the fade-out if enabled
void CBalloonHelp::HideBalloon(void)
{
	if(IsWindowVisible())
	{
		if(m_nAlpha > 250)
			m_nAlpha = 250;
		UINT_PTR i = SetTimer(ID_TIMER_HIDE, 25, NULL);
	}
}

// Removes the balloon from the screen immediately
void CBalloonHelp::HideBalloonImmediately(void)
{
	if(IsWindowVisible())
	{
		KillTimer(ID_TIMER_SHOW);
		ShowWindow(SW_HIDE);
	}
}

// Sets up the keyboard hook; adds this to list to close
void CBalloonHelp::SetKeyboardHook()
{
	s_KeyboardHookSection.Lock();
	// if hook is not yet set, set
	if ( NULL == s_hKeyboardHook )
	{
		s_hKeyboardHook = ::SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, NULL, ::GetCurrentThreadId());
	}
	s_apKeyboardCloseWnds.Add(m_hWnd);
	s_KeyboardHookSection.Unlock();
}


// Removes this from the list of windows to close; removes hook if not in use.
void CBalloonHelp::RemoveKeyboardHook()
{
	s_KeyboardHookSection.Lock();
	INT_PTR i;
	for (i=0; i < (int)s_apKeyboardCloseWnds.GetCount(); ++i)
	{
		if ( s_apKeyboardCloseWnds.GetAt(i) == m_hWnd )
			s_apKeyboardCloseWnds.RemoveAt(i--);
	}

	// if no more windows need to be notified, get rid of hook
	if ( s_apKeyboardCloseWnds.GetCount() == 0 && NULL != s_hKeyboardHook )
	{
		::UnhookWindowsHookEx(s_hKeyboardHook);
		s_hKeyboardHook = NULL;
	}
	s_KeyboardHookSection.Unlock();
}

// Erase client area of balloon
LRESULT CBalloonHelp::OnEraseBackground(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	HDC hdc = (HDC)wParam;
	CRect rect;
	GetClientRect(&rect);
	FillSolidRect(hdc, &rect, m_crBackground);
	return 0;
}

// draw balloon client area (title & contents)
LRESULT CBalloonHelp::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	PAINTSTRUCT ps;
	BeginPaint(&ps);
    CSize sizeHeader = DrawHeader(ps.hdc);
    DrawContent(ps.hdc, sizeHeader.cy + nTIP_MARGIN);
	EndPaint(&ps);
    return 0;
}

// draw balloon shape & boarder
LRESULT CBalloonHelp::OnNcPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	HDC hdc = GetWindowDC();

    // Paint into this DC
	CRect rect;
	GetWindowRect(&rect);
	ScreenToClient(&rect);
	CRect rectClient;
	GetClientRect(&rectClient);
	rectClient.OffsetRect(-rect.left, -rect.top);
	rect.OffsetRect(-rect.left, -rect.top);
	ExcludeClipRect(hdc, rectClient.left, rectClient.top, rectClient.right, rectClient.bottom);
	FillSolidRect(hdc, &rect, m_crBackground);

	_ASSERTE(m_rgnComplete);
	CBrush   brushFg;
	brushFg.CreateSolidBrush(m_crForeground);
	if ( m_unOptions & unSHOW_INNER_SHADOW )
	{
		CBrush   brushHL;
		// slightly lighter color
		int red = 170 + GetRValue(m_crBackground)/3;
		int green = 170 + GetGValue(m_crBackground)/3;
		int blue = 170 + GetBValue(m_crBackground)/3;
		brushHL.CreateSolidBrush(RGB(red,green,blue));
		OffsetRgn(m_rgnComplete, 1, 1);
		FrameRgn(hdc, m_rgnComplete, brushHL, 2, 2);
		// slightly darker color
		red = GetRValue(m_crForeground)/3 + GetRValue(m_crBackground)/3*2;
		green = GetGValue(m_crForeground)/3 + GetGValue(m_crBackground)/3*2;
		blue = GetBValue(m_crForeground)/3 + GetBValue(m_crBackground)/3*2;
		brushHL.DeleteObject();
		OffsetRgn(m_rgnComplete, -2, -2);
		brushHL.CreateSolidBrush(RGB(red,green,blue));
		FrameRgn(hdc, m_rgnComplete, brushHL, 2, 2);
		OffsetRgn(m_rgnComplete, 1, 1);
	}
	// outline
	FrameRgn(hdc, m_rgnComplete, brushFg, 1, 1);
	ReleaseDC(hdc);

	return 0;
}

// Close button handler
LRESULT CBalloonHelp::OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if (m_unOptions & unSHOW_CLOSE_BUTTON)
	{
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		if ( rect.PtInRect(point) )
		{
			m_uCloseState |= DFCS_PUSHED;
			SetCapture();
			OnMouseMove(uMsg, wParam, lParam, bHandled);
		}
	}
	return 0;
}

// Close button handler,
// close on LButton up handler
LRESULT CBalloonHelp::OnLButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if ( (m_unOptions & unSHOW_CLOSE_BUTTON) && (m_uCloseState & DFCS_PUSHED) )
	{
		ReleaseCapture();
		m_uCloseState &= ~DFCS_PUSHED;
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		if ( rect.PtInRect(point) )
			HideBalloon();
	}
	else if ( m_unOptions & unCLOSE_ON_LBUTTON_UP )
	{
		ReleaseCapture();
		HideBalloon();
	}
	else if ( !m_strURL.IsEmpty() )
	{
		HideBalloon();
		::ShellExecute(NULL, NULL, m_strURL, NULL, NULL, SW_SHOWNORMAL);
	}
	return 0;
}

// Close on RButton up checking
LRESULT CBalloonHelp::OnRButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if ( m_unOptions & unCLOSE_ON_RBUTTON_UP )
	{
		ReleaseCapture();
		HideBalloon();
	}
	return 0;
}

//
// do mouse tracking:
//   Close on mouse move;
//   Tracking for close button;
//
LRESULT CBalloonHelp::OnMouseMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	CPoint point(lParam);
	if (m_unOptions & unSHOW_CLOSE_BUTTON)
	{
		CRect rect;
		GetClientRect(&rect);
		rect.left = rect.right-16;
		rect.bottom = rect.top+16;
		UINT uState = DFCS_CAPTIONCLOSE | DFCS_FLAT;
		BOOL bPushed = m_uCloseState&DFCS_PUSHED;
		m_uCloseState &= ~DFCS_PUSHED;
		if ( rect.PtInRect(point) )
		{
			uState |= DFCS_HOT;
			if ( bPushed )
				uState |= DFCS_PUSHED;
			SetTimer(ID_TIMER_HOTTRACK, 500, NULL);
		}
		
		if ( uState != m_uCloseState )
		{
			CDC dc;
 			dc.Attach(GetDC());
			dc.DrawFrameControl(&rect, DFC_CAPTION, uState);
			m_uCloseState = uState;
			dc.Detach();
		}
		if ( bPushed )
			m_uCloseState |= DFCS_PUSHED;
	}

	if ( m_unOptions & unCLOSE_ON_MOUSE_MOVE )
	{
		if ( m_nAlpha == 255 && (abs(point.x-m_ptMouseOrig.x) > m_nMouseMoveTolerance || abs(point.y-m_ptMouseOrig.y) > m_nMouseMoveTolerance) )
			HideBalloon();
		else
			SetTimer(ID_TIMER_HOTTRACK, 100, NULL);
	}
	return 0;
}

// Ensures client area is the correct size relative to window size,
// presearves client contents if possible when moving.
LRESULT CBalloonHelp::OnNcCalcSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	BOOL bCalcValidRects = (BOOL)wParam;
	NCCALCSIZE_PARAMS FAR* lpncsp = (LPNCCALCSIZE_PARAMS)lParam;

	// nTIP_MARGIN pixel margin on all sides
   ::InflateRect(&lpncsp->rgrc[0], -nTIP_MARGIN,-nTIP_MARGIN);

   // nTIP_TAIL pixel "tail" on side closest to anchor
   switch ( GetBalloonQuadrant() )
   {
   case BQ_TOPRIGHT:
   case BQ_TOPLEFT:
      lpncsp->rgrc[0].top += nTIP_TAIL;
      break;
   case BQ_BOTTOMRIGHT:
   case BQ_BOTTOMLEFT:
      lpncsp->rgrc[0].bottom -= nTIP_TAIL;
      break;
   }

   // sanity: ensure rect does not have negative size
   if ( lpncsp->rgrc[0].right < lpncsp->rgrc[0].left )
      lpncsp->rgrc[0].right = lpncsp->rgrc[0].left;
   if ( lpncsp->rgrc[0].bottom < lpncsp->rgrc[0].top )
      lpncsp->rgrc[0].bottom = lpncsp->rgrc[0].top;

   if ( bCalcValidRects )
   {
      // determine if client position has changed relative to the window position
      // if so, don't bother presearving anything.
      if ( !::EqualRect(&lpncsp->rgrc[0], &lpncsp->rgrc[2]) )
      {
         ::SetRectEmpty(&lpncsp->rgrc[2]);
      }
   }
   return 0;
}

// handler for various timers
LRESULT CBalloonHelp::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
	UINT nIDEvent = (UINT)wParam;
   switch (nIDEvent)
   {
   case ID_TIMER_SHOW:
      m_nAlpha += 25;
      if ( m_nAlpha > 255 || m_unOptions&unDISABLE_FADEIN)
      {
         m_nAlpha = 255;
         KillTimer(ID_TIMER_SHOW);
         SetTimeout(m_unTimeout);     // start close timer
      }
      else   
         SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
      break;
   case ID_TIMER_HIDE:
      // just in case...
      KillTimer(ID_TIMER_SHOW);

      m_nAlpha -= 25;
      if ( m_nAlpha < 0 || m_unOptions&unDISABLE_FADEOUT )
      {
         m_nAlpha = 0;
         KillTimer(ID_TIMER_HIDE);
		 ShowWindow(SW_HIDE);
 //        DestroyWindow();
      }
      else
      {
         SetLayeredWindowAttributes(0, m_nAlpha, LWA_ALPHA);
      }
      break;
   case ID_TIMER_CLOSE:
      KillTimer(ID_TIMER_CLOSE);
      HideBalloon();
      break;
   case ID_TIMER_HOTTRACK:
      {
         CPoint point;
         KillTimer(ID_TIMER_HOTTRACK);
         ::GetCursorPos(&point);
         ScreenToClient(&point);
         OnMouseMove(uMsg, wParam, MAKEWORD(point.x, point.y), bHandled);
         break;
      }
   }
   return 0;
}

// Called as the window is being destroyed.  Completes destruction after removing keyboard hook.
LRESULT CBalloonHelp::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   // remove from list, if set
   RemoveKeyboardHook();
   return 0;
}

// close the balloon, performing any set transition effect.
LRESULT CBalloonHelp::OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   HideBalloon();
   return 0;
}

// Keyboard hook; used to implement the unCLOSE_ON_KEYPRESS option
LRESULT CALLBACK CBalloonHelp::KeyboardProc( int code, WPARAM wParam, LPARAM lParam)
{
	// Skip if the key was released or if it's a repeat
   if (code >= 0 && !(lParam & 0x80000000))
	{
		s_KeyboardHookSection.Lock();
		INT_PTR nNumWnds = CBalloonHelp::s_apKeyboardCloseWnds.GetCount();
		INT_PTR i;
		for (i=0; i<nNumWnds; ++i)
			::PostMessage((HWND)CBalloonHelp::s_apKeyboardCloseWnds.GetAt(i), WM_CLOSE, 0, 0);
		CBalloonHelp::s_apKeyboardCloseWnds.RemoveAll();
		s_KeyboardHookSection.Unlock();
	}
	return ::CallNextHookEx(CBalloonHelp::s_hKeyboardHook, code, wParam, lParam);
}

// Called after window has been destroyed.  Destroys the object if option is set.
void CBalloonHelp::OnFinalMessage(HWND)
{
   // free object if requested
   // be careful with this one :D
   if ( m_unOptions & unDELETE_THIS_ON_CLOSE )
      delete this;
}
